import torch
import torch.nn as nn
import torchvision

import models.conv_type
from utils.builder import Builder


def first_conv_bn(inp, oup, stride):
    return nn.Sequential(
        nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6(inplace=True)
    )


def conv_1x1_bn(builder, inp, oup):
    return nn.Sequential(
        builder.convbn1x1(inp, oup),
        nn.ReLU6(inplace=True)
    )


class InvertedResidual(nn.Module):
    def __init__(self, builder, in_channels, hidden_dim, out_channels, stride, expand_ratio):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        assert stride in [1, 2]

        self.use_res_connect = self.stride == 1 and in_channels == out_channels
        # print(self.use_res_connect)

        if expand_ratio == 1:
            self.conv = nn.Sequential(
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # pw-linear
                builder.convbn1x1(hidden_dim, out_channels),
            )
        else:
            self.conv = nn.Sequential(
                # pw
                builder.convbn1x1(in_channels, hidden_dim),
                nn.ReLU6(inplace=True),
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # pw-linear
                builder.convbn1x1(hidden_dim, out_channels),
            )

    def forward(self, x):
        if self.use_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)


class MobileNetV2(nn.Module):
    def __init__(self, builder, num_classes=1000, input_size=224, width_mult=1.):
        super(MobileNetV2, self).__init__()
        block = InvertedResidual
        layer_index = 0
        input_channel = 32
        last_channel = 1280
        interverted_residual_setting = [
            # t, c, n, s
            [1, 16, 1, 1],
            [6, 24, 1, 2],
            [6, 24, 1, 1],
            [6, 32, 1, 2],
            [6, 32, 1, 1],
            [6, 32, 1, 1],
            [6, 64, 1, 2],
            [6, 64, 1, 1],
            [6, 64, 1, 1],
            [6, 64, 1, 1],
            [6, 96, 1, 1],
            [6, 96, 1, 1],
            [6, 96, 1, 1],
            [6, 160, 1, 2],
            [6, 160, 1, 1],
            [6, 160, 1, 1],
            [6, 320, 1, 1],
        ]

        # building first layer
        assert input_size % 32 == 0
        input_channel = int(input_channel * width_mult)
        self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel
        self.features = [first_conv_bn(3, input_channel, 2)]
        # building inverted residual blocks
        lastc = 32
        for t, c, n, s in interverted_residual_setting:
            output_channel = int(c * width_mult)
            hidden_dim = input_channel * t
            self.features.append(block(builder, input_channel, hidden_dim, output_channel, s, expand_ratio=t))
            input_channel = output_channel
            layer_index += 1
            lastc = c

        # building last several layers
        self.features.append(conv_1x1_bn(builder, input_channel, self.last_channel))

        # make it nn.Sequential
        self.features = nn.Sequential(*self.features)
        self.pool = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Linear(self.last_channel, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = self.pool(x).flatten(1)
        x = self.classifier(x)
        return x


def mobilenet_v2(pretrained=False, builder=None, num_classes=1000):
    if pretrained:
        pass
    return MobileNetV2(builder=builder, num_classes=num_classes)


def mobilenet_v2_dense(pretrained=False, num_classes=1000):
    conv_bn_layer = getattr(models.conv_type, "DenseConv2dBN")
    nonlinearity = 'relu'
    builder = Builder(conv_bn_layer=conv_bn_layer, nonlinearity=nonlinearity)
    if pretrained:
        pass
    return MobileNetV2(builder=builder, num_classes=num_classes)


if __name__ == '__main__':
    conv_bn_layer = getattr(models.conv_type, "DenseConv2dBN")
    nonlinearity = 'relu'
    data = torch.ones([1, 3, 224, 224], dtype=torch.float32)
    builder = Builder(conv_bn_layer=conv_bn_layer, nonlinearity=nonlinearity)
    model1 = mobilenet_v2(builder=builder, num_classes=1000)
    model2 = torchvision.models.MobileNetV2()
    print(sum([p.numel() for p in model1.parameters()]))
    print(sum([p.numel() for p in model2.parameters()]))
    print(sum([p.numel() for p in model1.parameters()]))
    print(sum([p.numel() for p in model2.parameters()]))
    state_dict = {}
    for (key1, value1), (key2, value2) in zip(model1.state_dict().items(), model2.state_dict().items()):
        state_dict[key2] = value1.clone()
        assert value1.shape == value2.shape
    model2.load_state_dict(state_dict)
    model1.eval()
    model2.eval()
    output1 = model1(data)
    output2 = model2(data)
    print(torch.allclose(output1, output2))
